package com.googlecode.vmock.proxy;

import java.lang.reflect.Array;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import com.googlecode.vmock.exceptions.ProxyInstantiationException;

/**
 * Default implementation of a proxy factory, used by VMock.
 * 
 * @author marcin.jagus
 */
public class VMockProxyFactory extends ProxyFactory {

    @Override
    public <T> T createProxy(Class<T> clazz) {
        try {
            if (clazz.isInterface()) {
                return createInterfaceProxy(clazz);
            } else if (clazz.isArray()) {
                return createArrayProxy(clazz);
            } else if (Modifier.isAbstract(clazz.getModifiers())) {
                return createAbstractClassProxy(clazz);
            } else {
                return createConcreteClassProxy(clazz);
            }
        } catch (Exception e) {
            return handleInstantiationFailure(e);
        }
    }

    /**
     * Creates a JDK proxy for a given interface type.
     * 
     * @param clazz Given interface type
     * @return JDK proxy
     */
    protected <T> T createInterfaceProxy(Class<T> clazz) {
        return clazz.cast(Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class<?>[] { clazz }, NoAction.INSTANCE));
    }

    /**
     * Creates a CGLIB proxy for a given abstract class type.
     * 
     * @param clazz Given abstract class type
     * @return CGLIB proxy
     */
    protected <T> T createAbstractClassProxy(Class<T> clazz) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(NoAction.INSTANCE);
        return clazz.cast(enhancer.create());
    }

    /**
     * Creates a zero element array of a given array type.
     * 
     * @param clazz Given array type
     * @return Zero element array
     * @throws Exception
     */
    protected <T> T createArrayProxy(Class<T> clazz) {
        return clazz.cast(Array.newInstance(clazz.getComponentType(), 0));
    }

    /**
     * Creates a dummy object of a given concrete class type. The object is created without calling any of its constructors.
     * 
     * @param clazz Given concrete class type
     * @return Dummy object
     */
    protected <T> T createConcreteClassProxy(Class<T> clazz) {
        return SilentObjectCreator.createSilently(clazz);
    }

    /**
     * Wraps a given exception in {@link ProxyInstantiationException} object which is immediately thrown.
     * 
     * @param e Given exception
     * @return Nothing. Always throws exception.
     */
    private <T> T handleInstantiationFailure(Exception e) {
        throw new ProxyInstantiationException("Mock proxy cannot be created for some reason. See the root cause.", e);
    }

    /**
     * Implements both JDK and CGLIB proxy interfaces and almost always returns <code>null</code> from implemented methods. The only exception are
     * 'hashCode' and 'equals' methods which have the implementation similar to those of the {@link Object} class.
     * 
     * @author marcin.jagus
     */
    private static final class NoAction implements InvocationHandler, MethodInterceptor {

        private static final NoAction INSTANCE = new NoAction();

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            String name = method.getName();

            if (name.equals("hashCode")) {
                return System.identityHashCode(this);
            } else if (name.equals("equals")) {
                return this == args[0];
            } else if (name.equals("toString")) {
                return proxy.getClass().getName() + "@" + Integer.toHexString(System.identityHashCode(proxy));
            } else {
                return null;
            }
        }

        @Override
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            return proxy.invokeSuper(obj, args);
        }
    }

}
